package nl.Viewer.ViewManager.Views;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.concurrent.ConcurrentHashMap;

import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLJPanel;
import javax.media.opengl.glu.GLU;

import nl.Viewer.SerializedObjects.Parameter;
import nl.Viewer.ViewManager.Controllers.GenericController;
import nl.Viewer.ViewManager.Models.DataStores.ImageDataObject;
import nl.Viewer.ViewManager.Objects.RendererHelperFunctions;

/**
 * An implementation of a view which shows a three dimensional render which was rendered on the server. The view allows
 * manipulation of the orientation, zoom, twist, etc for requesting a new render.
 * 
 * @author Jeroen Verschuur
 * 
 */

public class JOGLImageDataObjectAsViewView extends GenericView implements GLEventListener {
   private static final long serialVersionUID            = -2958949544243327703L;

   /********************************************************************************************************************
    * 
    * Interaction properties
    * 
    */

   protected Point           lastLocation                = null;
   // set to true if dragging the mouse pointer out of the panel
   private boolean           dragOutOfScreen             = true;

   protected boolean         interactionAllowed          = false;

   protected Parameter       sourceObjectBoundsParameter = null;
   protected Parameter       eyeParameter                = null;
   protected Parameter       viewUpParameter             = null;
   protected Parameter       centerParameter             = null;
   protected Parameter       viewAngleParameter          = null;
   protected Parameter       clippingRangeParameter      = null;
   protected Parameter       azimuthParameter            = null;
   protected Parameter       elevationParameter          = null;
   protected Parameter       twistParameter              = null;
   protected Parameter       zoomParameter               = null;
   protected Parameter       viewSizeParameter           = null;

   protected double[]        sourceObjectBoundsValue     = new double[] { 0, 0, 0, 0, 0, 0 };
   protected double[]        eyeValue                    = new double[] { 0, 0, 0 };
   protected double[]        viewUpValue                 = new double[] { 0, 0, 0 };
   protected double[]        centerValue                 = new double[] { 0, 0, 0 };
   protected double          viewAngleValue              = 30;
   protected double[]        clippingRangeValue          = new double[] { 0, 0 };
   protected double          azimuthValue                = 0;
   protected double          elevationValue              = 0;
   protected double          twistValue                  = 0;
   protected double          zoomValue                   = 1;

   private boolean           parametersRetrieved         = false;

   /********************************************************************************************************************
    * 
    */

   protected int             panelWidth                  = 0, panelHeight = 0;
   protected float           aspect                      = 1.0f;
   protected boolean         interacting                 = false;
   protected int             imageHeight                 = 0, imageWidth = 0;
   protected GLU             glu                         = new GLU();
   protected ImageDataObject buf                         = null;

   protected float           minScale                    = 1;
   protected float           scale                       = 1;
   protected float           imageCenterX                = 0, imageCenterY = 0;

   protected GLJPanel        orientationPanel;

   public JOGLImageDataObjectAsViewView( GenericController parent ) {
      super( parent );
      this.renderPanel = new GLJPanel( getCapabilities() );
      this.orientationPanel = new GLJPanel( getCapabilities() );

      ( (GLJPanel)renderPanel ).addGLEventListener( this );
      renderPanel.addMouseListener( this );
      renderPanel.addMouseMotionListener( this );
      renderPanel.addMouseWheelListener( this );

      parent.getDataModel().addParameterModelChangeListener( this );
      parent.getDataModel().addDataStoreModelChangeListener( this );
   }

   public static GLCapabilities getCapabilities() {
      GLCapabilities c = new GLCapabilities();
      c.setDoubleBuffered( true );
      return c;
   }

   /**
    * Called by drawable to indicate mode or device has changed
    */
   public void displayChanged( GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged ) {
   }

   /**
    * Called after OpenGL is init'ed
    */
   public void init( GLAutoDrawable drawable ) {
      GL gl = drawable.getGL();
      // set erase color
      gl.glClearColor( 0.0f, 0.0f, 0.0f, 0.0f ); // white
      // set drawing color and point size
      gl.glColor3f( 0.0f, 0.0f, 0.0f );
      gl.glPointSize( 1.0f ); // a 'dot' is 4 by 4 pixel

      // enable alpha mask (import from gif sets alpha bits)
      gl.glBlendFunc( GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA );
      gl.glEnable( GL.GL_BLEND );

   }

   private boolean hasDrawn  = false;

   private int     oldX      = -1;
   private int     oldY      = -1;
   private int     oldWidth  = -1;
   private int     oldHeight = -1;

   /**
    * Called to indicate the drawing surface has been moved and/or resized
    */
   public void reshape( GLAutoDrawable drawable, int x, int y, int width, int height ) {
      GL gl = drawable.getGL();
      gl.glViewport( 0, 0, width, height );
      gl.glMatrixMode( GL.GL_PROJECTION );
      gl.glLoadIdentity();
      panelWidth = width;
      panelHeight = height;

      aspect = (float)panelWidth / (float)panelHeight;
      glu.gluOrtho2D( 0.0, width, 0.0, height );

      if( hasDrawn && ( x != oldX || y != oldY || width != oldWidth || height != oldHeight ) ) {
         System.out.println( "JOGLImagePanel requests update" );
         parent.requestNewRender();
      }
      oldX = x;
      oldY = y;
      oldWidth = width;
      oldHeight = height;
   }

   public void display() {
      ( (GLJPanel)renderPanel ).display();
   }

   /**
    * Called by drawable to initiate drawing
    */
   public void display( GLAutoDrawable drawable ) {
      GL gl = drawable.getGL();
      gl.glClear( GL.GL_COLOR_BUFFER_BIT );

      // 2D
      gl.glMatrixMode( GL.GL_PROJECTION );
      gl.glLoadIdentity();
      glu.gluOrtho2D( 0.0, panelWidth, 0.0, panelHeight );
      gl.glDisable( GL.GL_DEPTH_TEST );

      gl.glMatrixMode( GL.GL_MODELVIEW );
      gl.glLoadIdentity();

      drawImage( gl );

      if( interactionAllowed && interacting ) {
         // 3D if interacting
         gl.glMatrixMode( GL.GL_PROJECTION );
         gl.glLoadIdentity();
         glu.gluPerspective( viewAngleValue / zoomValue, aspect, 0.0001/* clippingRangeValue[0] */, 10000.0 /* clippingRangeValue[1] */);

         gl.glMatrixMode( GL.GL_MODELVIEW );
         gl.glLoadIdentity();

         glu.gluLookAt( eyeValue[0], eyeValue[1], eyeValue[2], centerValue[0], centerValue[1], centerValue[2],
               viewUpValue[0], viewUpValue[1], viewUpValue[2] );

         gl.glTranslated( centerValue[0], centerValue[1], centerValue[2] );

         gl.glRotated( -twistValue, 0, 0, 1 );
         gl.glRotated( elevationValue, 1, 0, 0 );
         gl.glRotated( -azimuthValue, 0, 1, 0 );

         gl.glTranslated( -centerValue[0], -centerValue[1], -centerValue[2] );

         RendererHelperFunctions.drawBox( gl, sourceObjectBoundsValue[0], sourceObjectBoundsValue[1],
               sourceObjectBoundsValue[2], sourceObjectBoundsValue[3], sourceObjectBoundsValue[4],
               sourceObjectBoundsValue[5] );
      }
      gl.glFlush();
      ( (GLJPanel)renderPanel ).swapBuffers();
   }

   public synchronized void drawImage( GL gl ) {
      hasDrawn = true;

      if( buf == null || buf.data == null )
         return;

      /*
       * calculate the offset needed to get the requested imageCenter in the center of the frame
       */
      Dimension frameDimension = ( (GLJPanel)renderPanel ).getSize();

      float scaledImageCenterX = imageCenterX * scale;
      float scaledImageCenterY = imageCenterY * ( scale );

      int screenCenterX = frameDimension.width / 2;
      int screenCenterY = frameDimension.height / 2;

      float xOffset = screenCenterX - scaledImageCenterX;
      float yOffset = screenCenterY - scaledImageCenterY;

      /*
       * NOTES To set a valid raster position outside the viewport, first set a valid raster position inside the
       * viewport, then call glBitmap with NULL as the bitmap parameter and with xmove and ymove set to the offsets of
       * the new raster position. This technique is useful when panning an image around the viewport.
       */
      gl.glPixelZoom( scale, scale );

      gl.glRasterPos2d( 0, 0 );
      gl.glBitmap( 0, 0, 0, 0, xOffset, yOffset, null );

      // System.out.println( "WxH: " + imageWidth + "x" + imageHeight + ", xOffset/yOffset: " + xOffset + "/" + yOffset
      // + ", scale=" + scale );

      if( interacting ) {
         gl.glPixelTransferf( GL.GL_RED_SCALE, 0.3f );
         gl.glPixelTransferf( GL.GL_GREEN_SCALE, 0.3f );
         gl.glPixelTransferf( GL.GL_BLUE_SCALE, 0.3f );
      }

      // we should always synchronize on the buffer to prevent access by for example the DataStore or other views
      synchronized( buf ) {
         buf.data.rewind();
         gl.glDrawPixels( imageWidth, imageHeight, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buf.data );
      }

      if( interacting ) {
         gl.glPixelTransferf( GL.GL_RED_SCALE, 1.0f );
         gl.glPixelTransferf( GL.GL_GREEN_SCALE, 1.0f );
         gl.glPixelTransferf( GL.GL_BLUE_SCALE, 1.0f );
      }

   }

   /*
    * Parameter methods
    * 
    */

   public void parameterValueChanged( Parameter p ) {
      synchronized( this ) {
         synchronizeParameterValues();
      }
   }

   public void dataStoreChanged( boolean newDataStore ) {
      synchronized( this ) {
         if( newDataStore ) {
            activateDataStore();
         } else {
            // just an update, do nothing for now
         }
      }
   }

   /**
    * Internal method, called by activateDataStore, to fetch all needed parameters from the session
    * 
    */
   private void getAllParameters() {
      if( parametersRetrieved )
         return;

      ConcurrentHashMap<String, Parameter> parameters = parent.getParameters();

      sourceObjectBoundsParameter = parameters.get( "SourceObjectBounds" );
      eyeParameter = parameters.get( "CameraPosition" );
      viewUpParameter = parameters.get( "CameraViewUp" );
      centerParameter = parameters.get( "CameraFocalPoint" );
      viewAngleParameter = parameters.get( "CameraViewAngle" );
      clippingRangeParameter = parameters.get( "CameraClippingRange" );
      azimuthParameter = parameters.get( "Azimuth" );
      elevationParameter = parameters.get( "Elevation" );
      twistParameter = parameters.get( "Twist" );
      zoomParameter = parameters.get( "Zoom" );
      viewSizeParameter = parameters.get( "ViewSize" );

      getRequiredParameterDefaults();

      parametersRetrieved = true;
   }

   /**
    * In this method all parameters for which we want the defaults from the database are specified
    * 
    */
   private void getRequiredParameterDefaults() {
      // same as the usual parameters
      synchronizeParameterValues();
   }

   /**
    * This method can set any parameter to a default value and override both database default- and source/pipeline
    * parameter-values
    * 
    */

   private void setFirstPreRenderParameters() {
      // this renderer overrides the values of all parameters
      if( viewSizeParameter != null ) {
         Dimension d = renderPanel.getSize();
         viewSizeParameter.setParameterValue( new Integer[] { d.width, d.height } );
         viewSizeParameter.setModified( true );
      }

      if( azimuthParameter != null ) {
         azimuthParameter.setParameterValue( azimuthValue );
         azimuthParameter.setModified( true );
      }

      if( elevationParameter != null ) {
         elevationParameter.setParameterValue( elevationValue );
         elevationParameter.setModified( true );
      }

      if( twistParameter != null ) {
         twistParameter.setParameterValue( -( twistValue ) );
         twistParameter.setModified( true );
      }

      if( zoomParameter != null ) {
         zoomParameter.setParameterValue( ( zoomValue ) );
         zoomParameter.setModified( true );
      }
   }

   public void setPreRenderParameters() {
      System.out.println( "setPreRenderParameters" );
      // this should actually be done in the constructor, however, the list of parameters is not yet retrieved when this
      // class is constructed

      boolean isFirstRender = false;
      if( !parametersRetrieved ) {
         isFirstRender = true;
      }

      // getAllParameters will set parametersRetrieved to true in case this was the first render
      getAllParameters();

      // For the JOGL Panel we want to expose the following interaction parameters to the server:
      // ViewSize (done in reshape), Azimuth, Elevation, Twist, Zoom

      if( isFirstRender ) {
         setFirstPreRenderParameters();
      } else {
         // in this case, the same parameters need to be set as with the first render
         setFirstPreRenderParameters();
      }
   }

   private void synchronizeParameterValues() {
      if( sourceObjectBoundsParameter != null )
         sourceObjectBoundsValue = GenericView.objectToDoubleArray( sourceObjectBoundsParameter.getParameterValue() );

      if( eyeParameter != null )
         eyeValue = GenericView.objectToDoubleArray( eyeParameter.getParameterValue() );

      if( viewUpParameter != null )
         viewUpValue = GenericView.objectToDoubleArray( viewUpParameter.getParameterValue() );

      if( centerParameter != null )
         centerValue = GenericView.objectToDoubleArray( centerParameter.getParameterValue() );

      if( viewAngleParameter != null )
         viewAngleValue = GenericView.objectToDouble( viewAngleParameter.getParameterValue() );

      if( clippingRangeParameter != null )
         clippingRangeValue = GenericView.objectToDoubleArray( clippingRangeParameter.getParameterValue() );

      if( azimuthParameter != null )
         azimuthValue = GenericView.objectToDouble( azimuthParameter.getParameterValue() );

      if( elevationParameter != null )
         elevationValue = GenericView.objectToDouble( elevationParameter.getParameterValue() );

      if( zoomParameter != null )
         zoomValue = GenericView.objectToDouble( zoomParameter.getParameterValue() );

      if( twistParameter != null )
         twistValue = GenericView.objectToDouble( twistParameter.getParameterValue() );

      super.synchronizeViews();

   }

   public synchronized void activateDataStore() {
      getAllParameters();

      Object currentData = parent.getDataStore().getCurrentData();
      if( currentData == null ) {
         System.out.println( "No datastore available yet" );
      } else {
         if( currentData instanceof ImageDataObject == false ) {
            System.out.println( "MUST have ImageDataObject's in datastore to use this renderer (we have " + currentData.getClass() + ")" );
         } else {
            setBuffer( (ImageDataObject)currentData );
         }
      }
   }

   private void setBuffer( ImageDataObject ido ) {
      imageWidth = ido.width;
      imageHeight = ido.height;
      imageCenterX = ido.width / 2;
      imageCenterY = ido.height / 2;

      buf = ido;

      // get new viewing parameters
      synchronizeParameterValues();

      interactionAllowed = true;
   }

   public float moveUp( float up ) {
      float tempCenter = imageCenterY - ( up / scale );
      if( tempCenter <= -( imageHeight / 2 ) ) {
         imageCenterY = -( imageHeight / 2 ) + 1;
      } else if( tempCenter >= ( imageHeight + ( imageHeight / 2 ) ) ) {
         imageCenterY = imageHeight + ( imageHeight / 2 ) - 1;
      } else {
         imageCenterY = tempCenter;
      }
      return imageCenterY;
   }

   public float moveRight( float right ) {
      float tempCenter = imageCenterX - ( right / scale );
      if( tempCenter <= -( imageWidth / 2 ) ) {
         imageCenterX = -( imageWidth / 2 ) + 1;
      } else if( tempCenter >= ( imageWidth + ( imageWidth / 2 ) ) ) {
         imageCenterX = ( imageWidth + ( imageWidth / 2 ) ) - 1;
      } else {
         imageCenterX = tempCenter;
      }
      return imageCenterX;
   }

   public float scale( float factor ) {
      float tempScale = scale * factor;
      if( tempScale < minScale ) {
         this.scale = minScale;
      } else {
         this.scale = tempScale;
      }
      return scale;
   }

   /*
    * 'implement' the methods that should be implemented, extending classes can override just those methods that are
    * required
    * 
    */

   public void mouseReleased( MouseEvent event ) {
      // reset last mouse location
      lastLocation = null;
      interacting = false;
      if( event.getButton() != MouseEvent.BUTTON2 ) {
         parent.requestNewRender();
      }
   }

   public void mousePressed( MouseEvent event ) {
//      if( event.getButton() == MouseEvent.BUTTON2 ) {
//         // synchronize with another view if possible
//         ArrayList<GenericController> pairs = Application.viewManager.synchronizableControllers.findPairsForController( parent );
//         System.out.println( "Trying to synchronize with " + pairs.size() + " controllers" );
//         if( pairs.size() > 0 )
//            parent.synchronizeWith( (GenericController)pairs.get( 0 ) );
//
//         return;
//      }
      // save last location on screen
      interacting = true;
      lastLocation = event.getLocationOnScreen();
   }

   public void mouseDragged( MouseEvent event ) {
      if( event.getButton() == MouseEvent.BUTTON2 )
         return;

      if( !dragOutOfScreen && !( (GLJPanel)renderPanel ).contains( event.getPoint() ) ) {
         // System.out.println("Out of bounds");
         return;
      }

      if( lastLocation == null )
         return; // this should/can not happen!

      Point p = event.getLocationOnScreen();

      int xDist = ( p.x - lastLocation.x );
      int yDist = ( p.y - lastLocation.y );

      // check which mouse button was used
      if( ( event.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK ) == MouseEvent.BUTTON1_DOWN_MASK ) {
         if( ( event.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK ) == MouseEvent.SHIFT_DOWN_MASK ) {
            // rotate around the view vector
            // create two vectors: one from the center to the start of the drag
            // and one from the center to the end of the drag
            // the angle between those two vectors is the angle for the rotation

            Point componentTopLeft = ( (GLJPanel)renderPanel ).getLocationOnScreen();
            int xCenter = ( ( ( (GLJPanel)renderPanel ).getX() + ( (GLJPanel)renderPanel ).getWidth() ) / 2 ) + componentTopLeft.x;
            int yCenter = ( ( ( (GLJPanel)renderPanel ).getY() + ( (GLJPanel)renderPanel ).getHeight() ) / 2 ) + componentTopLeft.y;

            float angle = (float)( ( Math.atan2( yCenter - p.y, p.x - xCenter ) - Math.atan2( yCenter - lastLocation.y,
                  lastLocation.x - xCenter ) ) * ( 180 / Math.PI ) );
            // twist -= angle;

            increaseTwist( -angle );

            // System.out.println("Twist changed to "+twist);

         } else {
            increaseAzimuth( -( 180 * (float)xDist / (float)( (GLJPanel)renderPanel ).getWidth() ) );

            /*
             * if( elevation > -90 && elevation < 90 ) { // azimuth -= 180*(float)xDist/(float)thisPanel.getWidth();
             * increaseAzimuth( -( 180 * (float) xDist / (float) getWidth() ) ); } else { increaseAzimuth( 180 * (float)
             * xDist / (float) getWidth() ); // azimuth += 180*(float)xDist/(float)thisPanel.getWidth(); }
             */
            // elevation += (180*(float)yDist/(float)thisPanel.getHeight()/2);
            increaseElevation( ( 180 * (float)yDist / (float)( (GLJPanel)renderPanel ).getHeight() / 2 ) );
            // System.out.println( "Azimuth/elevation changed to " + azimuthValue + "/" + elevationValue );

         }
      }

      if( ( event.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK ) == MouseEvent.BUTTON3_DOWN_MASK ) {
         if( ( event.getModifiersEx() & MouseEvent.SHIFT_DOWN_MASK ) == MouseEvent.SHIFT_DOWN_MASK ) {
            // pan (somewhat proportionally to the zoom-in factor)
            // translateX -= (float)xDist/(20*Math.max(1, 20-translateZ));
            // translateY += (float)yDist/(20*Math.max(1, 20-translateZ));
            // System.out.println("TranslateX/Y changed to "+translateX+"/"+translateZ);
            /*
             * increaseTranslation( -(float)xDist / ( 20 * Math.max( 1, 20 - translateZ ) ), (float)yDist / ( 20 *
             * Math.max( 1, 20 - translateZ ) ) );
             */
         } else {
            // zoom by changing the distance
            // System.out.println("eye[2] = "+eye[2]+", center[2] ="+center[2]);

            double relative = -( (double)yDist / (double)( (GLJPanel)renderPanel ).getHeight() );// * ( eye[2] -
            // center[2] );

            increaseZoom( relative );
         }
      }

      lastLocation = event.getLocationOnScreen();

      display();
   }

   public void mouseWheelMoved( MouseWheelEvent event ) {
      // translateZ += (float)event.getWheelRotation();
      increaseZoom( -(double)event.getWheelRotation() * 0.1 );
      // briefly show the zoomed in/out bounding box
      interacting = true;
      display();
      interacting = false;
      parent.requestNewRender();
   }

   public void increaseZoom( double difference ) {
      zoomValue += difference;
   }

   public void increaseTranslation( float x, float y ) {
      /*
       * translateXValue[0] += x; translateYValue[0] += y;
       */
   }

   public void increaseAzimuth( float dif ) {
      azimuthValue += dif;
      azimuthValue %= 360;
      /*
       * totalazimuth += dif; totalazimuth %= 360; System.out.println( "Total azimuth: " + totalazimuth );
       */
   }

   public void increaseElevation( float dif ) {
      elevationValue += dif;
      elevationValue %= 360;
      if( elevationValue < -180 ) {
         elevationValue = 360 - elevationValue;
      }
   }

   public void increaseTwist( float dif ) {
      twistValue += dif;
      twistValue %= 360;
   }

}
